﻿using Bystd.NetFactory.Protocols.Exceptions;
using Bystd.NetFactory.Protocols.Quic.Connections;
using Bystd.NetFactory.Protocols.Quic.Events;
using Bystd.NetFactory.Protocols.Quic.Frames;
using Bystd.NetFactory.Protocols.Quic.InternalInfrastructure;
using Bystd.NetFactory.Protocols.Quic.PacketProcessing;
using Bystd.NetFactory.Protocols.Quic.Packets;
using Bystd.NetFactory.Protocols.Quic.Settings;
using Bystd.NetFactory.Protocols.Quic.Streams;
using Bystd.NetFactory.Protocols.Quic.Utilities;
using Bystd.NetFactory.Udp;
using System;
using System.Collections.Generic;
using System.Net;

namespace Bystd.NetFactory.Quic
{
    /// <summary>
    /// Source of quic module project https://github.com/Vect0rZ/Quic.NET
    /// quic client
    /// </summary>
    public class QuicClientProvider
    {
        INetClientProvider netProvider = null;
        private PacketWireTransfer _pwt;
        private InitialPacketCreator _packetCreator;
        private UInt64 _maximumStreams = QuicSettings.MaximumStreamId;
        private QuicConnection _connection;
        private QuicStream stream;
        private bool isConnected = false;

        public ClientConnectedEvent OnConnected;
        public ConnectionClosedEvent OnDisconnected;
        public StreamDataReceivedEvent OnDataReceived;

        public QuicClientProvider(int concurrentNumber = 8)
        {
            netProvider = NetClientProvider.CreateProvider(4096, concurrentNumber, NetProviderType.Udp);
            netProvider.DisconnectedHandler = Disconnected;
            netProvider.ReceivedOffsetHandler = OnReceived;
            netProvider.ConnectedHandler = Connected;

            _packetCreator = new InitialPacketCreator();
        }

        public void Connect(int port ,string ip)
        {
            var  _peerIp = new IPEndPoint(IPAddress.Parse(ip), port);

            // Initialize packet reader
            _pwt = new PacketWireTransfer(netProvider, _peerIp);

            // Start initial protocol process
            InitialPacket connectionPacket = _packetCreator.CreateInitialPacket(0, 0);

            netProvider.Connect(port, ip);
            
            // Send the initial packet
            _pwt.Send(connectionPacket);
        }

        public bool Send(byte[] buffer)
        {
            int retry = 0,timeout=30;

            while (isConnected == false && retry<timeout)
            {
                retry++;
                System.Threading.Thread.Sleep(1000);
            }
            if (isConnected == false)
                throw new Exception($"it's connect timeout {timeout}s");

            stream = _connection.CreateStream(StreamType.ClientBidirectional);

            return stream.Send(buffer);
        }

        public bool Send(QuicStream stream,byte[] buffer)
        {
            int retry = 0, timeout = 30;

            while (isConnected == false && retry < timeout)
            {
                retry++;
                System.Threading.Thread.Sleep(1000);
            }
            if (isConnected == false)
                throw new Exception($"it's connect timeout {timeout}s");

            return stream.Send(buffer);
        }

        private void OnReceived(SegmentToken sToken)
        {
            // Await response for sucessfull connection creation by the server
            Packet packet = null;

            packet = _pwt.Read(sToken.Data.buffer, sToken.Data.size, sToken.Data.offset);

            if (packet is InitialPacket initP)
            {
                HandleInitialFrames(initP);
                _connection = EstablishConnection(initP.SourceConnectionId, initP.SourceConnectionId);

                _connection.OnConnectionClosed += StreamClosed;
                stream = _connection.CreateStream(StreamType.ClientBidirectional);
                OnConnected?.Invoke(_connection);
                isConnected = true;
            }
            else
            {
                byte[] buff = stream.Receive(sToken.Data.buffer, sToken.Data.size, sToken.Data.offset);
                OnDataReceived?.Invoke(sToken.sToken, stream, buff);
            }
        }

        private void StreamClosed(QuicConnection connection)
        {
            isConnected = false;
            OnDisconnected?.Invoke(connection);
        }

        private void Disconnected(SocketToken sToken)
        {
            isConnected = false;
        }

        private void Connected(SocketToken sToken,bool isOk)
        {

        }

        /// <summary>
        /// Handles initial packet's frames. (In most cases protocol frames)
        /// </summary>
        /// <param name="packet"></param>
        private void HandleInitialFrames(Packet packet)
        {
            List<Frame> frames = packet.GetFrames();
            for (int i = frames.Count - 1; i > 0; i--)
            {
                Frame frame = frames[i];
                if (frame is ConnectionCloseFrame ccf)
                {
                    throw new QuicConnectivityException(ccf.ReasonPhrase);
                }

                if (frame is MaxStreamsFrame msf)
                {
                    _maximumStreams = msf.MaximumStreams.Value;
                }

                // Break out if the first Padding Frame has been reached
                if (frame is PaddingFrame)
                    break;
            }
        }

        /// <summary>
        /// Create a new connection
        /// </summary>
        /// <param name="connectionId"></param>
        /// <param name="peerConnectionId"></param>
        private QuicConnection EstablishConnection(GranularInteger connectionId, GranularInteger peerConnectionId)
        {
            ConnectionData connection = new ConnectionData(_pwt, connectionId, peerConnectionId);
            return new QuicConnection(connection);
        }
    }
}
